
# 分箱
import mglearn.datasets
import numpy as np
import pandas as pd
from IPython.core.display_functions import display
from matplotlib import pyplot as plt
from sklearn.ensemble import RandomForestClassifier, RandomForestRegressor
from sklearn.feature_selection import SelectPercentile, SelectFromModel, RFE
from sklearn.linear_model import LogisticRegression, LinearRegression, Ridge
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import OneHotEncoder, PolynomialFeatures, MinMaxScaler
from sklearn.svm import SVR
from sklearn.datasets import load_diabetes, load_breast_cancer


# 分箱、离散化、线性模型与树
'''
数据的最佳表示方法取决于数据的语义和所使用的模型。
线性模型和基于树的模型（如决策树、梯度提升树、随机森林）是两类常用模型，它们在处理不同特征表示时表现不同。
'''

'''
线性模型只能对线性关系建模，对于单个特征的情况就是直线。决策树可以构建更为复杂的数据模型，但这强烈依赖于数据表示。
有一种方法可以让线性模型在连续数据上变得更加强大，就是使用特征分箱（binning，也叫离散化，即 discretization）将其
划分为多个特征，如下所述
'''

'''
假设将特征的输入范围（如从 -3 到 3）划分为固定数量的箱子（例如 10 个），数据点可以用它所在的箱子来表示。
为此，我们使用 np.linspace 函数创建 11 个边界点，从而定义 10 个均匀分布的箱子。
'''

X,y = mglearn.datasets.make_wave(n_samples=100)
print(f'X.shape:{X.shape}')
line = np.linspace(-3,3,1000,endpoint=False).reshape(-1,1)

bins = np.linspace(-3,3,11)
print("bins: {}".format(bins))
# bins: [-3.  -2.4 -1.8 -1.2 -0.6  0.   0.6  1.2  1.8  2.4  3. ]
# 这里第一个箱子包含特征取值在 -3 到 -2.4 之间的所有数据点，第二个箱子包含特征取值
# 在 -2.4 到 -1.8 之间的所有数据点，以此类推。

# 接下来，我们记录每个数据点所属的箱子。这可以用 np.digitize 函数轻松计算出来：

which_bin = np.digitize(X,bins=bins)
print("\nData points:\n", X[:5])
print("\nBin membership for data points:\n", which_bin[:5])

'''
我们将 wave 数据集中的单个连续特征转换为分类特征，表示数据点所在的箱子。
为了在 scikit-learn 中使用这个离散特征，我们用 preprocessing.OneHotEncoder 将其转换为 one-hot 编码。
OneHotEncoder 的编码方式与 pandas.get_dummies 相同，但它目前只适用于整数值的分类变量。
'''

encoder = OneHotEncoder(sparse_output=False)
encoder.fit(which_bin)
X_binned = encoder.transform(which_bin)
print(X_binned[:5])

# 下面我们在 one-hot 编码后的数据上构建新的线性模型和新的决策树模型。结果见图
# 箱子的边界由黑色虚线表示：

line_binned = encoder.transform(np.digitize(line,bins=bins))

reg = LinearRegression()
reg.fit(X_binned,y)
plt.plot(line,reg.predict(line_binned),label='linear regression binned')
plt.plot(X[:,0],y,'o',c='k')
plt.vlines(bins, -3, 3, linewidth=1, alpha=.2)
plt.legend(loc="best")
plt.ylabel("Regression output")
plt.xlabel("Input feature")
# plt.show()


'''
1. 模型预测的重合
 虚线和实线完全重合：这说明在分箱处理后，线性回归模型和决策树模型做出了完全相同的预测。这表明在每个分箱内，两个模型都预测了一个常数值。
 每个箱子内的特征不变：由于每个分箱内的特征值是不变的，因此对于一个分箱内的所有数据点，任何模型都会预测相同的值。
2. 模型灵活性的变化
 线性模型变得更加灵活：分箱处理后，线性模型对每个分箱具有不同的取值，从而变得更加灵活。这是因为分箱处理将特征空间划分成了多个小区域，
 每个区域内的数据点具有相似的特征值，线性模型可以为每个区域学习一个不同的预测值。
 决策树模型的灵活性降低：分箱处理后，决策树模型的灵活性反而降低了。这是因为决策树本身已经具有很强的分箱能力，它可以学习在任何位置划分数据。
 分箱处理限制了决策树的灵活性，因为它将特征空间预先划分成了固定的区域。
3. 模型的特性
 决策树模型：
  学习分箱：决策树可以学习如何分箱对预测这些数据最为有用。它通过递归地划分特征空间，找到最优的划分点，从而构建决策树。
  多特征考虑：决策树可以同时查看多个特征，而分箱通常针对的是单个特征。这使得决策树在处理多特征数据时具有更强的灵活性和表达能力。
 线性模型：
  表现力提升：分箱处理后，线性模型的表现力得到了极大的提高。这是因为分箱处理将非线性特征转换成了线性可分的特征，使得线性模型能够更好地拟合数据。
  适用场景：对于特定的数据集，如果有充分的理由使用线性模型（比如数据集很大、维度很高，但有些特征与输出的关系是非线性的），
  那么分箱是提高建模能力的好方法。
4. 总结
 分箱处理对线性模型有益：分箱处理可以将非线性特征转换成线性可分的特征，从而提高线性模型的灵活性和表现力。
 分箱处理对决策树模型影响不大：决策树模型本身已经具有很强的分箱能力，分箱处理反而可能限制其灵活性。决策树可以同时考虑多个特征，
 而分箱通常针对单个特征。
 适用场景：如果数据集很大、维度很高，但某些特征与输出的关系是非线性的，分箱处理可以显著提高线性模型的建模能力。
'''

# 交互特征与多项式特征
'''
为了丰富特征表示，特别是对于线性模型，可以添加交互特征和多项式特征。这种特征工程在统计建模和机器学习中都很常见。
例如，线性模型可以在分箱数据上学习每个箱子的常数值，但也可以通过重新加入原始特征来学习斜率，从而得到一个更高维的数据集。
'''

X_combined = np.hstack([X, X_binned])

reg = LinearRegression().fit(X_combined, y)
line_combined = np.hstack([line, line_binned])
plt.plot(line, reg.predict(line_combined), label='linear regression combined')
for bin in bins:
 plt.plot([bin, bin], [-3, 3], ':', c='k')
plt.legend(loc="best")
plt.ylabel("Regression output")
plt.xlabel("Input feature")
plt.plot(X[:, 0], y, 'o', c='k')
# plt.show()

'''
在这个例子中，模型在每个箱子中学习了一个偏移和一个斜率。但由于只有一个 x 轴特征，所有箱子的斜率都相同，这限制了模型的灵活性。
为了使每个箱子有不同的斜率，可以添加交互特征，即箱子指示符与原始特征的乘积。这样可以创建一个更丰富的数据集。
'''

X_product = np.hstack([X_binned,X*X_binned])
print(X_product.shape)

'''
这个数据集现在包含 20 个特征：每个箱子的指示符、原始特征，以及它们的乘积。
乘积特征可以看作是每个箱子中 x 轴特征的单独副本，在箱子内等于原始特征，在其他位置为零。下图展示了线性模型在这种新表示下的结果。
'''

reg = LinearRegression().fit(X_product, y)
line_product = np.hstack([line_binned,line*line_binned])
plt.plot(line,reg.predict(line_product),label='Linear regression product')

for bin in bins:
 plt.plot([bin,bin],[-3,3],':',c='k')
plt.plot(X[:, 0], y, 'o', c='k')
plt.ylabel("Regression output")
plt.xlabel("Input feature")
plt.legend(loc="best")
# plt.show()

'''
如你所见，现在这个模型中每个箱子都有自己的偏移和斜率。使用分箱是扩展连续特征的一种方法。 
另一种方法是使用原始特征的多项式（polynomial）。对于给定特征 x，我们可以考虑 x ** 2、x ** 3、x ** 4，等等。
这在preprocessing 模块的 PolynomialFeatures 中实现：
'''

# 包含直到x ** 10的多项式:
# 默认的"include_bias=True"添加恒等于1的常数特征
poly = PolynomialFeatures(degree=10, include_bias=False)
poly.fit(X)
X_poly = poly.transform(X)

# 多项式的次数为 10，因此生成了 10 个特征：
print("X_poly.shape: {}".format(X_poly.shape))
# X_poly.shape: (100, 10)

# 比较 X_poly 和 X 的元素
print("Entries of X:\n{}".format(X[:5]))
print("Entries of X_poly:\n{}".format(X_poly[:5]))

# 可以通过调用 get_feature_names 方法来获取特征的语义，给出每个特征的指数：
print("Polynomial feature names:\n{}".format(poly.get_feature_names_out()))
# Polynomial feature names:
# ['x0' 'x0^2' 'x0^3' 'x0^4' 'x0^5' 'x0^6' 'x0^7' 'x0^8' 'x0^9' 'x0^10']

# 将多项式特征与线性回归模型一起使用，可以得到经典的多项式回归（polynomial regression）模型
reg = LinearRegression().fit(X_poly,y)
line_poly = poly.transform(line)
plt.plot(line, reg.predict(line_poly), label='polynomial linear regression')
plt.plot(X[:, 0], y, 'o', c='k')
plt.ylabel("Regression output")
plt.xlabel("Input feature")
plt.legend(loc="best")
plt.show()

'''
如你所见，多项式特征在这个一维数据上得到了非常平滑的拟合。但高次多项式在边界上
或数据很少的区域可能有极端的表现。
'''

# 作为对比，下面是在原始数据上学到的核 SVM 模型，没有做任何变换
for gamma in [1,10]:
 svr = SVR(gamma=gamma).fit(X,y)
 plt.plot(line, svr.predict(line), label='SVR gamma={}'.format(gamma))

plt.plot(X[:, 0], y, 'o', c='k')
plt.ylabel("Regression output")
plt.xlabel("Input feature")
plt.legend(loc="best")
plt.show()

'''
使用更加复杂的模型（即核 SVM），我们能够学到一个与多项式回归的复杂度类似的预测
结果，且不需要进行显式的特征变换。
'''

'''
我们再次观察波士顿房价数据集，作为对交互特征和多项式特征更加实际的应用。我们在第 2 章已经在这个数据集上使用过多项式特征了。
现在来看一下这些特征的构造方式，以及多项式特征的帮助有多大。首先加载数据，然后利用 MinMaxScaler 将其缩放到 0 和 1之间：
'''

diabetes = load_diabetes()
X_train,X_test,y_train,y_test = train_test_split(diabetes.data,diabetes.target,random_state=0)
scaler = MinMaxScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.fit_transform(X_test)

# 下面我们提取多项式特征和交互特征，次数最高为 2：
poly = PolynomialFeatures(degree=2).fit(X_train_scaled)
X_train_poly = poly.transform(X_train_scaled)
X_test_poly = poly.transform(X_test_scaled)
print("X_train.shape: {}".format(X_train.shape))
print("X_train_poly.shape: {}".format(X_train_poly.shape))

'''
原始数据有 13 个特征，现在被扩展到 105 个交互特征。这些新特征表示两个不同的原始特征之间所有可能的交互项，以及每个原始特征的平方。
这里 degree=2 的意思是，我们需要由最多两个原始特征的乘积组成的所有特征。
利用 get_feature_names 方法可以得到输入特征和输出特征之间的确切对应关系：
'''
print("Polynomial feature names:\n{}".format(poly.get_feature_names_out()))

'''
第一个新特征是常数特征，这里的名称是 "1"。接下来的 13 个特征是原始特征（名称从"x0" 到 "x12"）。
然后是第一个特征的平方（"x0^2"）以及它与其他特征的组合。
'''

# 我们对 Ridge 在有交互特征的数据上和没有交互特征的数据上的性能进行对比：
ridge = Ridge().fit(X_train_scaled, y_train)
print("Score without interactions: {:.3f}".format(
 ridge.score(X_test_scaled, y_test)))
ridge = Ridge().fit(X_train_poly, y_train)
print("Score with interactions: {:.3f}".format(
 ridge.score(X_test_poly, y_test)))

# 如果使用更加复杂的模型（比如随机森林），情况会稍有不同：
rf = RandomForestRegressor(n_estimators=100).fit(X_train_scaled, y_train)
print("Score without interactions: {:.3f}".format(
 rf.score(X_test_scaled, y_test)))
rf = RandomForestRegressor(n_estimators=100).fit(X_train_poly, y_train)
print("Score with interactions: {:.3f}".format(rf.score(X_test_poly, y_test)))

# 单变量非线性变换
'''
我们刚刚看到，添加特征的平方或立方可以改进线性回归模型。其他变换通常也对变换某
些特征有用，特别是应用数学函数，比如 log、exp 或 sin。虽然基于树的模型只关注特征
的顺序，但线性模型和神经网络依赖于每个特征的尺度和分布。如果在特征和目标之间存
在非线性关系，那么建模就变得非常困难，特别是对于回归问题。log 和 exp 函数可以帮
助调节数据的相对比例，从而改进线性模型或神经网络的学习效果。我们在第 2 章中对内
存价格数据应用过这种函数。在处理具有周期性模式的数据时，sin 和 cos 函数非常有用。
'''

'''
大部分模型都在每个特征（在回归问题中还包括目标值）大致遵循高斯分布时表现最好，
也就是说，每个特征的直方图应该具有类似于熟悉的“钟形曲线”的形状。使用诸如 log
和 exp 之类的变换并不稀奇，但却是实现这一点的简单又有效的方法。在一种特别常见的
情况下，这样的变换非常有用，就是处理整数计数数据时。计数数据是指类似“用户 A 多
长时间登录一次？”这样的特征。计数不可能取负值，并且通常遵循特定的统计模式。下
面我们使用一个模拟的计数数据集，其性质与在自然状态下能找到的数据集类似。特征全
都是整数值，而响应是连续的：
'''

# 设置随机数生成器
rnd = np.random.RandomState(0)
# 生成原始数据 X_org
# 这行代码生成了一个形状为 (1000, 3) 的二维数组 X_org，
# 其中每个元素都是从标准正态分布（均值为 0，标准差为 1）中随机抽取的
X_org = rnd.normal(size=(1000,3))
# 生成权重 w
w = rnd.normal(size=3)
# 生成特征数据 X
# poisson 方法生成泊松分布的随机数。
X = rnd.poisson(10 * np.exp(X_org))
# 生成目标变量 y
# X_org 是输入特征，w 是权重
y = np.dot(X_org,w)

# 如果我们计算每个值的出现次数，那么数值的分布将变得更清楚：
print("Number of feature appearances:\n{}".format(np.bincount(X[:, 0])))

bins = np.bincount(X[:,0])
# plt.bar(range(len(bins)), bins, color='w')
# plt.ylabel("Number of appearances")
# plt.xlabel("Value")
# plt.show()

'''
特征 X[:, 1] 和 X[:, 2] 具有类似的性质。这种类型的数值分布（许多较小的值和一些非
常大的值）在实践中非常常见。但大多数线性模型无法很好地处理这种数据。我们尝试拟合一个岭回归模型：
'''
X_train,X_test,y_train,y_test = train_test_split(X,y,random_state=0)
score = Ridge().fit(X_train,y_train).score(X_test,y_test)
print("Test score: {:.3f}".format(score))
# Test score: 0.622

# 你可以从相对较小的 R2 分数中看出，Ridge 无法真正捕捉到 X 和 y 之间的关系。不过应用
# 对数变换可能有用。由于数据取值中包括 0（对数在 0 处没有定义），所以我们不能直接应
# 用 log，而是要计算 log(X + 1)：
X_train_log = np.log(X_train+1)
X_test_log = np.log(X_test+1)

print(X_train_log)
print(X_test_log)
# 变换之后，数据分布的不对称性变小，也不再有非常大的异常值
plt.hist(X_train_log[:, 0], bins=25, color='gray')
plt.ylabel("Number of appearances")
plt.xlabel("Value")
plt.show()

# 在新数据上构建一个岭回归模型，可以得到更好的拟合：
score = Ridge().fit(X_train_log,y_train).score(X_test_log,y_test)
print("Test score: {:.3f}".format(score))
# Test score: 0.875

'''
寻找数据集和模型组合的最佳变换是一门艺术。在实际中，通常只有部分特征需要变换，且不同特征的变换方式可能不同。
对于基于树的模型，变换不重要，但对线性模型可能至关重要，对回归目标变量 y 进行变换有时也有用，如预测计数任务中使用 log(y + 1) 变换。
分箱、多项式和交互项对模型性能影响大，尤其是复杂度低的模型，如线性模型和朴素贝叶斯模型，基于树的模型通常能自己发现重要交互项，无需显式变换数据，
而 SVM、最近邻和神经网络等模型有时也会从这些变换中受益，但效果不如线性模型明显。
'''